/*
   Copyright (c) 2007, 2024, Oracle and/or its affiliates.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License, version 2.0,
   as published by the Free Software Foundation.

   This program is designed to work with certain software (including
   but not limited to OpenSSL) that is licensed under separate terms,
   as designated in a particular file or component or in included license
   documentation.  The authors of MySQL hereby grant you an additional
   permission to link the program and your derivative works with the
   separately licensed software that they have either included with
   the program or referenced in the documentation.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License, version 2.0, for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
*/

#include <time.h>
#include "util/require.h"

#include <portlib/NdbSleep.h>
#include <portlib/NdbDir.hpp>
#include "atrt.hpp"
#include "portlib/ndb_compiler.h"

static bool generate_my_cnf(BaseString mycnf, atrt_config &config);
static bool create_directory(const char *path);
static bool delete_file_if_exists(const char *path);
static bool copy_file(const char *src, const char *dst);
static bool generate_mysql_init_file_configurations(const char *fileName);

bool setup_directories(atrt_config &config, int setup) {
  /**
   * 0 = validate
   * 1 = setup
   * 2 = setup+clean
   */
  for (unsigned i = 0; i < config.m_clusters.size(); i++) {
    atrt_cluster &cluster = *config.m_clusters[i];
    for (unsigned j = 0; j < cluster.m_processes.size(); j++) {
      atrt_process &proc = *cluster.m_processes[j];
      const char *dir = proc.m_proc.m_cwd.c_str();
      struct stat sbuf;
      int exists = 0;
      if (lstat(dir, &sbuf) == 0) {
        if (S_ISDIR(sbuf.st_mode))
          exists = 1;
        else
          exists = -1;
      }

      switch (setup) {
        case 0:
          switch (exists) {
            case 0:
              g_logger.error("Could not find directory: %s", dir);
              return false;
            case -1:
              g_logger.error("%s is not a directory!", dir);
              return false;
          }
          break;
        case 1:
          if (exists == -1) {
            g_logger.error("%s is not a directory!", dir);
            return false;
          }
          break;
        case 2:
          if (exists == 1) {
            if (!remove_dir(dir)) {
              g_logger.error("Failed to remove %s!", dir);
              return false;
            }
            exists = 0;
            break;
          } else if (exists == -1) {
            if (!unlink(dir)) {
              g_logger.error("Failed to remove %s!", dir);
              return false;
            }
            exists = 0;
          }
      }
      if (exists != 1) {
        if (!create_directory(dir)) {
          return false;
        }
      }
    }
  }
  return true;
}

static void printfile(FILE *out, Properties &props, const char *section, ...)
    ATTRIBUTE_FORMAT(printf, 3, 4);

static void printfile(FILE *out, Properties &props, const char *section, ...) {
  Properties::Iterator it(&props);
  const char *name = it.first();
  if (name) {
    va_list ap;
    va_start(ap, section);
    /* const int ret = */ vfprintf(out, section, ap);
    va_end(ap);
    fprintf(out, "\n");

    for (; name; name = it.next()) {
      const char *val;
      props.get(name, &val);
      fprintf(out, "%s %s\n", name + 2, val);
    }
    fprintf(out, "\n");
  }
  fflush(out);
}

#ifdef _WIN32
#define popen _popen
#define pclose _pclose
#endif

static bool generate_my_cnf(BaseString mycnf, atrt_config &config) {
  FILE *out = fopen(mycnf.c_str(), "a+");
  if (out == NULL) {
    g_logger.error("Failed to open %s for append", mycnf.c_str());
    return false;
  }

  time_t now = time(0);
  fprintf(out, "#\n# Generated by atrt\n");
  fprintf(out, "# %s\n", ctime(&now));

  for (unsigned i = 0; i < config.m_clusters.size(); i++) {
    atrt_cluster &cluster = *config.m_clusters[i];
    printfile(out, cluster.m_options.m_generated, "[mysql_cluster%s]",
              cluster.m_name.c_str());

    for (unsigned j = 0; j < cluster.m_processes.size(); j++) {
      atrt_process &proc = *cluster.m_processes[j];

      switch (proc.m_type) {
        case atrt_process::AP_NDB_MGMD:
          printfile(out, proc.m_options.m_generated,
                    "[cluster_config.ndb_mgmd.%d%s]", proc.m_index,
                    proc.m_cluster->m_name.c_str());
          break;
        case atrt_process::AP_NDBD:
          printfile(out, proc.m_options.m_generated,
                    "[cluster_config.ndbd.%d%s]", proc.m_index,
                    proc.m_cluster->m_name.c_str());
          break;
        case atrt_process::AP_MYSQLD:
          printfile(out, proc.m_options.m_generated, "[mysqld.%d%s]",
                    proc.m_index, proc.m_cluster->m_name.c_str());
          break;
        case atrt_process::AP_CLIENT:
          printfile(out, proc.m_options.m_generated, "[client.%d%s]",
                    proc.m_index, proc.m_cluster->m_name.c_str());
          break;
        case atrt_process::AP_CUSTOM:
          printfile(out, proc.m_options.m_generated, "[%s.%d%s]",
                    proc.m_name.c_str(), proc.m_index,
                    proc.m_cluster->m_name.c_str());
          break;
        case atrt_process::AP_NDB_API:
        // fall-through
        case atrt_process::AP_ALL:
        // fall-through
        case atrt_process::AP_CLUSTER:
          break;
      }
    }
  }

  fclose(out);
  return true;
}

bool exists_file(const char *path) {
  struct stat sbuf;
  int ret = lstat(path, &sbuf);
  return ret == 0;
}

static bool delete_file_if_exists(const char *path) {
  if (!exists_file(path)) {
    return true;
  }

  if (unlink(path) != 0) {
    g_logger.error("Failed to remove %s", path);
    return false;
  }

  return true;
}

static bool copy_file(const char *src, const char *dst) {
  BaseString cp;
  cp.assfmt("cp %s %s", src, dst);
  to_fwd_slashes(cp);
  if (sh(cp.c_str()) != 0) {
    g_logger.error("Failed to '%s'", cp.c_str());
    return false;
  }

  return true;
}

bool setup_files(atrt_config &config, int setup, int sshx) {
  /**
   * 0 = validate
   * 1 = setup
   * 2 = setup+clean
   */
  BaseString mycnf;
  mycnf.assfmt("%s/my.cnf", g_basedir);
  to_native(mycnf);

  if (!create_directory(g_basedir)) {
    return false;
  }

  if (mycnf != g_my_cnf) {
    if (!delete_file_if_exists(mycnf.c_str())) {
      return false;
    }

    BaseString aux(g_my_cnf);
    to_native(aux);
    if (!copy_file(aux.c_str(), mycnf.c_str())) {
      return false;
    }
  }

  if (config.m_config_type == atrt_config::INI) {
    for (unsigned i = 0; i < config.m_clusters.size(); i++) {
      atrt_cluster &cluster = *config.m_clusters[i];
      const char *cluster_name = cluster.m_name.c_str();

      if (strcmp(cluster_name, ".atrt") == 0) {
        continue;
      }

      BaseString dst_config_ini;
      dst_config_ini.assfmt("%s/config%s.ini", g_basedir, cluster_name);
      to_native(dst_config_ini);

      if (!delete_file_if_exists(dst_config_ini.c_str())) {
        return false;
      }

      BaseString src_config_ini;
      src_config_ini.assfmt("%s/config%s.ini", g_cwd, cluster_name);
      to_native(src_config_ini);

      if (!copy_file(src_config_ini.c_str(), dst_config_ini.c_str())) {
        return false;
      }
    }
  }

  if (setup == 2 || config.m_generated) {
    bool use_mysqld =
        (g_resources.getExecutableFullPath(g_resources.MYSQL_INSTALL_DB) == "");
    if (!use_mysqld) {
      // Even if mysql_install_db exists, prefer use of mysqld if possible
      BaseString mysqld_bin_path =
          g_resources.getExecutableFullPath(g_resources.MYSQLD).c_str();
      BaseString tmp;
      tmp.assfmt("%s --help --verbose", mysqld_bin_path.c_str());
      FILE *f = popen(tmp.c_str(), "re");
      char buf[1000];
      while (NULL != fgets(buf, sizeof(buf), f)) {
        if (strncmp(buf, "initialize-insecure ", 20) == 0) {
          use_mysqld = true;
        }
      }
      pclose(f);
    }
    /**
     * Do mysql_install_db
     */
    for (unsigned i = 0; i < config.m_clusters.size(); i++) {
      atrt_cluster &cluster = *config.m_clusters[i];
      for (unsigned j = 0; j < cluster.m_processes.size(); j++) {
        atrt_process &proc = *cluster.m_processes[j];
        if (proc.m_type == atrt_process::AP_MYSQLD)
#ifndef _WIN32
        {
          const char *val;
          require(proc.m_options.m_loaded.get("--datadir=", &val));
          BaseString tmp;
          if (use_mysqld) {
            BaseString initFile;
            initFile.assfmt("%s/mysqld-init-file.sql", g_basedir);
            if (!generate_mysql_init_file_configurations(initFile.c_str())) {
              g_logger.error(
                  "Failed to generate the mysql configuration init-file: %s",
                  initFile.c_str());
              return false;
            }

            BaseString mysqld_bin_path =
                g_resources.getExecutableFullPath(g_resources.MYSQLD).c_str();
            tmp.assfmt(
                "%s --defaults-file=%s/my.cnf --basedir=%s "
                "--datadir=%s --initialize-insecure --init-file=%s"
                "> %s/mysqld-initialize.log 2>&1",
                mysqld_bin_path.c_str(), g_basedir, g_prefix, val,
                initFile.c_str(), proc.m_proc.m_cwd.c_str());
          } else {
            BaseString mysql_install_db_bin_path =
                g_resources.getExecutableFullPath(g_resources.MYSQL_INSTALL_DB)
                    .c_str();
            assert(mysql_install_db_bin_path != "");
            tmp.assfmt(
                "%s --defaults-file=%s/my.cnf --basedir=%s "
                "--datadir=%s > %s/mysql_install_db.log 2>&1",
                mysql_install_db_bin_path.c_str(), g_basedir, g_prefix0, val,
                proc.m_proc.m_cwd.c_str());
          }
          to_fwd_slashes(tmp);
          if (sh(tmp.c_str()) != 0) {
            if (use_mysqld) {
              g_logger.error(
                  "Failed to mysqld --initialize-insecure for "
                  "%s, cmd: '%s'",
                  proc.m_proc.m_cwd.c_str(), tmp.c_str());
            } else {
              g_logger.error("Failed to mysql_install_db for %s, cmd: '%s'",
                             proc.m_proc.m_cwd.c_str(), tmp.c_str());
            }
            return false;
          } else {
            if (use_mysqld) {
              g_logger.info("mysqld --initialize-insecure for %s",
                            proc.m_proc.m_cwd.c_str());
            } else {
              g_logger.info("mysql_install_db for %s",
                            proc.m_proc.m_cwd.c_str());
            }
          }
        }
#else
        {
          g_logger.info(
              "not running mysqld --initialize-insecure nor "
              "mysql_install_db for %s",
              proc.m_proc.m_cwd.c_str());
        }
#endif
      }
    }
  }

  bool skip_my_cnf_generation =
      config.m_config_type == atrt_config::INI || config.m_generated == false;
  if (skip_my_cnf_generation) {
    g_logger.info("Skipping my.cnf generation...");
  } else {
    bool ok = generate_my_cnf(mycnf, config);
    if (!ok) return false;
  }

  for (unsigned i = 0; i < config.m_clusters.size(); i++) {
    atrt_cluster &cluster = *config.m_clusters[i];
    for (unsigned j = 0; j < cluster.m_processes.size(); j++) {
      atrt_process &proc = *cluster.m_processes[j];

      /**
       * Create env.sh
       */
      BaseString tmp;
      tmp.assfmt("%s/env.sh", proc.m_proc.m_cwd.c_str());
      to_native(tmp);
      char **env = BaseString::argify(0, proc.m_proc.m_env.c_str());
      if (env[0] || proc.m_proc.m_path.length()) {
        Vector<BaseString> keys;
        FILE *fenv = fopen(tmp.c_str(), "w+");
        if (fenv == 0) {
          g_logger.error("Failed to open %s for writing", tmp.c_str());
          return false;
        }
        for (size_t k = 0; env[k]; k++) {
          tmp = env[k];
          ssize_t pos = tmp.indexOf('=');
          require(pos > 0);
          env[k][pos] = 0;
          fprintf(fenv, "%s=\"%s\"\n", env[k], env[k] + pos + 1);
          keys.push_back(env[k]);
          free(env[k]);
        }
        if (proc.m_proc.m_path.length()) {
          fprintf(fenv, "CMD=\"%s", proc.m_proc.m_path.c_str());
          if (proc.m_proc.m_args.length()) {
            fprintf(fenv, " %s", proc.m_proc.m_args.c_str());
          }
          fprintf(fenv, "\"\nexport CMD\n");
        }

        fprintf(fenv, "PATH=");
        for (int i = 0; g_search_path[i] != 0; i++) {
          fprintf(fenv, "%s/%s:", g_prefix0, g_search_path[i]);
        }
        fprintf(fenv, "$PATH\n");
        keys.push_back("PATH");

        {
          /**
           * In 5.5...binaries aren't compiled with rpath
           * So we need an explicit LD_LIBRARY_PATH
           *
           * Use path from libmysqlclient.so
           */
#if defined(__MACH__)
          BaseString libdir =
              g_resources.getLibraryDirectory(g_resources.LIBMYSQLCLIENT_DYLIB)
                  .c_str();
          fprintf(fenv, "DYLD_LIBRARY_PATH=%s:$DYLD_LIBRARY_PATH\n",
                  libdir.c_str());
          keys.push_back("DYLD_LIBRARY_PATH");
#else
          BaseString libdir =
              g_resources.getLibraryDirectory(g_resources.LIBMYSQLCLIENT_SO)
                  .c_str();
          fprintf(fenv, "LD_LIBRARY_PATH=%s:$LD_LIBRARY_PATH\n",
                  libdir.c_str());
          keys.push_back("LD_LIBRARY_PATH");
#endif
        }

        for (unsigned k = 0; k < keys.size(); k++)
          fprintf(fenv, "export %s\n", keys[k].c_str());

        fflush(fenv);
        fclose(fenv);
      }
      free(env);

      {
        tmp.assfmt("%s/ssh-login.sh", proc.m_proc.m_cwd.c_str());
        FILE *fenv = fopen(tmp.c_str(), "w+");
        if (fenv == 0) {
          g_logger.error("Failed to open %s for writing", tmp.c_str());
          return false;
        }
        fprintf(fenv, "#!/bin/sh\n");
        fprintf(fenv, "cd %s\n", proc.m_proc.m_cwd.c_str());
        fprintf(fenv, "[ -f /etc/profile ] && . /etc/profile\n");
        fprintf(fenv, ". ./env.sh\n");
        fprintf(fenv, "ulimit -Sc unlimited\n");
        fprintf(fenv, "bash -i");
        fflush(fenv);
        fclose(fenv);
      }
    }
  }

  return true;
}

static bool create_directory(const char *path) {
  BaseString native(path);
  to_native(native);
  BaseString tmp(path);
  Vector<BaseString> list;

  if (tmp.split(list, "/") == 0) {
    g_logger.error("Failed to create directory: %s", tmp.c_str());
    return false;
  }

  BaseString cwd = IF_WIN("", "/");
  for (unsigned i = 0; i < list.size(); i++) {
    cwd.append(list[i].c_str());
    cwd.append("/");
    NdbDir::create(cwd.c_str(), NdbDir::u_rwx() | NdbDir::g_r() | NdbDir::g_x(),
                   true);
  }

  struct stat sbuf;
  if (lstat(native.c_str(), &sbuf) != 0 || !S_ISDIR(sbuf.st_mode)) {
    g_logger.error("Failed to create directory: %s (%s)", native.c_str(),
                   cwd.c_str());
    return false;
  }

  return true;
}

bool remove_dir(const char *path, bool inclusive) {
  if (access(path, 0)) return true;

  const int max_retries = 20;
  int attempt = 0;

  while (true) {
    if (NdbDir::remove_recursive(path, !inclusive)) return true;

    attempt++;
    if (attempt > max_retries) {
      g_logger.error("Failed to remove directory '%s'!", path);
      return false;
    }

    g_logger.warning(
        " - attempt %d to remove directory '%s' failed "
        ", retrying...",
        attempt, path);

    NdbSleep_MilliSleep(100);
  }

  abort();  // Never reached
  return false;
}

bool generate_mysql_init_file_configurations(const char *fileName) {
  FILE *fp = fopen(fileName, "w");
  if (fp == NULL) {
    g_logger.error("Failed to open mysql init file: %s", fileName);
    return false;
  }

  if (fprintf(fp, "rename user root@localhost to root@'%%';") < 0) {
    g_logger.error("Failed to write to mysql init file: %s", fileName);
    return false;
  }

  if (fclose(fp) == EOF) {
    g_logger.error("Failed to close mysql init file: %s", fileName);
    return false;
  }

  return true;
}
